﻿/*************************************************************************************

   Extended WPF Toolkit

   Copyright (C) 2007-2013 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://wpftoolkit.codeplex.com/license 

   For more features, controls, and fast professional support,
   pick up the Plus Edition at http://xceed.com/wpf_toolkit

   Stay informed: follow @datagrid on Twitter or Like http://facebook.com/datagrids

  ***********************************************************************************/

/**************************************************************************\
    Copyright Microsoft Corporation. All Rights Reserved.
\**************************************************************************/

namespace Microsoft.Windows.Shell
{
  using System;
  using System.Collections.Generic;
  using System.Diagnostics.CodeAnalysis;
  using System.Runtime.InteropServices;
  using System.Windows;
  using System.Windows.Interop;
  using System.Windows.Media;
  using System.Windows.Threading;
  using Standard;

  using HANDLE_MESSAGE = System.Collections.Generic.KeyValuePair<Standard.WM, Standard.MessageHandler>;
  using System.Windows.Controls.Primitives;

  internal class WindowChromeWorker : DependencyObject
  {
    // Delegate signature used for Dispatcher.BeginInvoke.
    private delegate void _Action();

    #region Fields

    private const SWP _SwpFlags = SWP.FRAMECHANGED | SWP.NOSIZE | SWP.NOMOVE | SWP.NOZORDER | SWP.NOOWNERZORDER | SWP.NOACTIVATE;

    private readonly List<HANDLE_MESSAGE> _messageTable;

    /// <summary>The Window that's chrome is being modified.</summary>
    private Window _window;
    /// <summary>Underlying HWND for the _window.</summary>
    private IntPtr _hwnd;
    private HwndSource _hwndSource = null;
    private bool _isHooked = false;

    // These fields are for tracking workarounds for WPF 3.5SP1 behaviors.
    private bool _isFixedUp = false;
    private bool _isUserResizing = false;
    private bool _hasUserMovedWindow = false;
    private Point _windowPosAtStartOfUserMove = default( Point );

    // Field to track attempts to force off Device Bitmaps on Win7.
    private int _blackGlassFixupAttemptCount;

    /// <summary>Object that describes the current modifications being made to the chrome.</summary>
    private WindowChrome _chromeInfo;

    // Keep track of this so we can detect when we need to apply changes.  Tracking these separately
    // as I've seen using just one cause things to get enough out of sync that occasionally the caption will redraw.
    private WindowState _lastRoundingState;
    private WindowState _lastMenuState;
    private bool _isGlassEnabled;

    #endregion

    public WindowChromeWorker()
    {
      _messageTable = new List<HANDLE_MESSAGE>
            {
                new HANDLE_MESSAGE(WM.SETTEXT,               _HandleSetTextOrIcon),
                new HANDLE_MESSAGE(WM.SETICON,               _HandleSetTextOrIcon),
                new HANDLE_MESSAGE(WM.NCACTIVATE,            _HandleNCActivate),
                new HANDLE_MESSAGE(WM.NCCALCSIZE,            _HandleNCCalcSize),
                new HANDLE_MESSAGE(WM.NCHITTEST,             _HandleNCHitTest),
                new HANDLE_MESSAGE(WM.NCRBUTTONUP,           _HandleNCRButtonUp),
                new HANDLE_MESSAGE(WM.SIZE,                  _HandleSize),
                new HANDLE_MESSAGE(WM.WINDOWPOSCHANGED,      _HandleWindowPosChanged),
                new HANDLE_MESSAGE(WM.DWMCOMPOSITIONCHANGED, _HandleDwmCompositionChanged),
            };

      if( Utility.IsPresentationFrameworkVersionLessThan4 )
      {
        _messageTable.AddRange( new[]
        {
                   new HANDLE_MESSAGE(WM.SETTINGCHANGE,         _HandleSettingChange),
                   new HANDLE_MESSAGE(WM.ENTERSIZEMOVE,         _HandleEnterSizeMove),
                   new HANDLE_MESSAGE(WM.EXITSIZEMOVE,          _HandleExitSizeMove),
                   new HANDLE_MESSAGE(WM.MOVE,                  _HandleMove),
                } );
      }
    }

    public void SetWindowChrome( WindowChrome newChrome )
    {
      VerifyAccess();
      Assert.IsNotNull( _window );

      if( newChrome == _chromeInfo )
      {
        // Nothing's changed.
        return;
      }

      if( _chromeInfo != null )
      {
        _chromeInfo.PropertyChangedThatRequiresRepaint -= _OnChromePropertyChangedThatRequiresRepaint;
      }

      _chromeInfo = newChrome;
      if( _chromeInfo != null )
      {
        _chromeInfo.PropertyChangedThatRequiresRepaint += _OnChromePropertyChangedThatRequiresRepaint;
      }

      _ApplyNewCustomChrome();
    }

    private void _OnChromePropertyChangedThatRequiresRepaint( object sender, EventArgs e )
    {
      _UpdateFrameState( true );
    }

    public static readonly DependencyProperty WindowChromeWorkerProperty = DependencyProperty.RegisterAttached(
        "WindowChromeWorker",
        typeof( WindowChromeWorker ),
        typeof( WindowChromeWorker ),
        new PropertyMetadata( null, _OnChromeWorkerChanged ) );

    private static void _OnChromeWorkerChanged( DependencyObject d, DependencyPropertyChangedEventArgs e )
    {
      var w = ( Window )d;
      var cw = ( WindowChromeWorker )e.NewValue;

      // The WindowChromeWorker object should only be set on the window once, and never to null.
      Assert.IsNotNull( w );
      Assert.IsNotNull( cw );
      Assert.IsNull( cw._window );

      cw._SetWindow( w );
    }

    private void _SetWindow( Window window )
    {
      Assert.IsNull( _window );
      Assert.IsNotNull( window );

      _window = window;

      // There are potentially a couple funny states here.
      // The window may have been shown and closed, in which case it's no longer usable.
      // We shouldn't add any hooks in that case, just exit early.
      // If the window hasn't yet been shown, then we need to make sure to remove hooks after it's closed.
      _hwnd = new WindowInteropHelper( _window ).Handle;

      if( Utility.IsPresentationFrameworkVersionLessThan4 )
      {
        // On older versions of the framework the client size of the window is incorrectly calculated.
        // We need to modify the template to fix this on behalf of the user.
        Utility.AddDependencyPropertyChangeListener( _window, Window.TemplateProperty, _OnWindowPropertyChangedThatRequiresTemplateFixup );
        Utility.AddDependencyPropertyChangeListener( _window, Window.FlowDirectionProperty, _OnWindowPropertyChangedThatRequiresTemplateFixup );
      }

      _window.Closed += _UnsetWindow;

      // Use whether we can get an HWND to determine if the Window has been loaded.
      if( IntPtr.Zero != _hwnd )
      {
        // We've seen that the HwndSource can't always be retrieved from the HWND, so cache it early.
        // Specifically it seems to sometimes disappear when the OS theme is changing.
        _hwndSource = HwndSource.FromHwnd( _hwnd );
        Assert.IsNotNull( _hwndSource );
        _window.ApplyTemplate();

        if( _chromeInfo != null )
        {
          _ApplyNewCustomChrome();
        }
      }
      else
      {
        _window.SourceInitialized += ( sender, e ) =>
        {
          _hwnd = new WindowInteropHelper( _window ).Handle;
          Assert.IsNotDefault( _hwnd );
          _hwndSource = HwndSource.FromHwnd( _hwnd );
          Assert.IsNotNull( _hwndSource );

          if( _chromeInfo != null )
          {
            _ApplyNewCustomChrome();
          }
        };
      }
    }

    private void _UnsetWindow( object sender, EventArgs e )
    {
      if( Utility.IsPresentationFrameworkVersionLessThan4 )
      {
        Utility.RemoveDependencyPropertyChangeListener( _window, Window.TemplateProperty, _OnWindowPropertyChangedThatRequiresTemplateFixup );
        Utility.RemoveDependencyPropertyChangeListener( _window, Window.FlowDirectionProperty, _OnWindowPropertyChangedThatRequiresTemplateFixup );
      }

      if( _chromeInfo != null )
      {
        _chromeInfo.PropertyChangedThatRequiresRepaint -= _OnChromePropertyChangedThatRequiresRepaint;
      }

      _RestoreStandardChromeState( true );
    }

    [SuppressMessage( "Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters" )]
    public static WindowChromeWorker GetWindowChromeWorker( Window window )
    {
      Verify.IsNotNull( window, "window" );
      return ( WindowChromeWorker )window.GetValue( WindowChromeWorkerProperty );
    }

    [SuppressMessage( "Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters" )]
    public static void SetWindowChromeWorker( Window window, WindowChromeWorker chrome )
    {
      Verify.IsNotNull( window, "window" );
      window.SetValue( WindowChromeWorkerProperty, chrome );
    }

    private void _OnWindowPropertyChangedThatRequiresTemplateFixup( object sender, EventArgs e )
    {
      Assert.IsTrue( Utility.IsPresentationFrameworkVersionLessThan4 );

      if( _chromeInfo != null && _hwnd != IntPtr.Zero )
      {
        // Assume that when the template changes it's going to be applied.
        // We don't have a good way to externally hook into the template
        // actually being applied, so we asynchronously post the fixup operation
        // at Loaded priority, so it's expected that the visual tree will be
        // updated before _FixupFrameworkIssues is called.
        _window.Dispatcher.BeginInvoke( DispatcherPriority.Loaded, ( _Action )_FixupFrameworkIssues );
      }
    }

    private void _ApplyNewCustomChrome()
    {
      if( _hwnd == IntPtr.Zero )
      {
        // Not yet hooked.
        return;
      }

      if( _chromeInfo == null )
      {
        _RestoreStandardChromeState( false );
        return;
      }

      if( !_isHooked )
      {
        _hwndSource.AddHook( _WndProc );
        _isHooked = true;
      }

      _FixupFrameworkIssues();

      // Force this the first time.
      _UpdateSystemMenu( _window.WindowState );
      _UpdateFrameState( true );

      NativeMethods.SetWindowPos( _hwnd, IntPtr.Zero, 0, 0, 0, 0, _SwpFlags );
    }

    private void _FixupFrameworkIssues()
    {
      Assert.IsNotNull( _chromeInfo );
      Assert.IsNotNull( _window );

      // This margin is only necessary if the client rect is going to be calculated incorrectly by WPF.
      // This bug was fixed in V4 of the framework.
      if( !Utility.IsPresentationFrameworkVersionLessThan4 )
      {
        return;
      }

      if( _window.Template == null )
      {
        // Nothing to fixup yet.  This will get called again when a template does get set.
        return;
      }

      // Guard against the visual tree being empty.
      if( VisualTreeHelper.GetChildrenCount( _window ) == 0 )
      {
        // The template isn't null, but we don't have a visual tree.
        // Hope that ApplyTemplate is in the queue and repost this, because there's not much we can do right now.
        _window.Dispatcher.BeginInvoke( DispatcherPriority.Loaded, ( _Action )_FixupFrameworkIssues );
        return;
      }

      var rootElement = ( FrameworkElement )VisualTreeHelper.GetChild( _window, 0 );

      RECT rcWindow = NativeMethods.GetWindowRect( _hwnd );
      RECT rcAdjustedClient = _GetAdjustedWindowRect( rcWindow );

      Rect rcLogicalWindow = DpiHelper.DeviceRectToLogical( new Rect( rcWindow.Left, rcWindow.Top, rcWindow.Width, rcWindow.Height ) );
      Rect rcLogicalClient = DpiHelper.DeviceRectToLogical( new Rect( rcAdjustedClient.Left, rcAdjustedClient.Top, rcAdjustedClient.Width, rcAdjustedClient.Height ) );

      Thickness nonClientThickness = new Thickness(
         rcLogicalWindow.Left - rcLogicalClient.Left,
         rcLogicalWindow.Top - rcLogicalClient.Top,
         rcLogicalClient.Right - rcLogicalWindow.Right,
         rcLogicalClient.Bottom - rcLogicalWindow.Bottom );

      if( rootElement != null )
      {
        rootElement.Margin = new Thickness(
            0,
            0,
            -( nonClientThickness.Left + nonClientThickness.Right ),
            -( nonClientThickness.Top + nonClientThickness.Bottom ) );
      }

      // The negative thickness on the margin doesn't properly get applied in RTL layouts.
      // The width is right, but there is a black bar on the right.
      // To fix this we just add an additional RenderTransform to the root element.
      // This works fine, but if the window is dynamically changing its FlowDirection then this can have really bizarre side effects.
      // This will mostly work if the FlowDirection is dynamically changed, but there aren't many real scenarios that would call for
      // that so I'm not addressing the rest of the quirkiness.
      if( rootElement != null )
      {
        if( _window.FlowDirection == FlowDirection.RightToLeft )
        {
          rootElement.RenderTransform = new MatrixTransform( 1, 0, 0, 1, -( nonClientThickness.Left + nonClientThickness.Right ), 0 );
        }
        else
        {
          rootElement.RenderTransform = null;
        }
      }

      if( !_isFixedUp )
      {
        _hasUserMovedWindow = false;
        _window.StateChanged += _FixupRestoreBounds;

        _isFixedUp = true;
      }
    }

    // There was a regression in DWM in Windows 7 with regard to handling WM_NCCALCSIZE to effect custom chrome.
    // When windows with glass are maximized on a multimonitor setup the glass frame tends to turn black.
    // Also when windows are resized they tend to flicker black, sometimes staying that way until resized again.
    //
    // This appears to be a bug in DWM related to device bitmap optimizations.  At least on RTM Win7 we can
    // evoke a legacy code path that bypasses the bug by calling an esoteric DWM function.  This doesn't affect
    // the system, just the application.
    // WPF also tends to call this function anyways during animations, so we're just forcing the issue
    // consistently and a bit earlier.
    [SuppressMessage( "Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes" )]
    private void _FixupWindows7Issues()
    {
      if( _blackGlassFixupAttemptCount > 5 )
      {
        // Don't keep trying if there's an endemic problem with this.
        return;
      }

      if( Utility.IsOSWindows7OrNewer && NativeMethods.DwmIsCompositionEnabled() )
      {
        ++_blackGlassFixupAttemptCount;

        bool success = false;
        try
        {
          DWM_TIMING_INFO? dti = NativeMethods.DwmGetCompositionTimingInfo( _hwnd );
          success = dti != null;
        }
        catch( Exception )
        {
          // We aren't sure of all the reasons this could fail.
          // If we find new ones we should consider making the NativeMethod swallow them as well.
          // Since we have a limited number of retries and this method isn't actually critical, just repost.

          // Disabling this for the published code to reduce debug noise.  This will get compiled away for retail binaries anyways.
          //Assert.Fail(e.Message);
        }

        // NativeMethods.DwmGetCompositionTimingInfo swallows E_PENDING.
        // If the call wasn't successful, try again later.
        if( !success )
        {
          Dispatcher.BeginInvoke( DispatcherPriority.Loaded, ( _Action )_FixupWindows7Issues );
        }
        else
        {
          // Reset this.  We will want to force this again if DWM composition changes.
          _blackGlassFixupAttemptCount = 0;
        }
      }
    }

    private void _FixupRestoreBounds( object sender, EventArgs e )
    {
      Assert.IsTrue( Utility.IsPresentationFrameworkVersionLessThan4 );
      if( _window.WindowState == WindowState.Maximized || _window.WindowState == WindowState.Minimized )
      {
        // Old versions of WPF sometimes force their incorrect idea of the Window's location
        // on the Win32 restore bounds.  If we have reason to think this is the case, then
        // try to undo what WPF did after it has done its thing.
        if( _hasUserMovedWindow )
        {
          _hasUserMovedWindow = false;
          WINDOWPLACEMENT wp = NativeMethods.GetWindowPlacement( _hwnd );

          RECT adjustedDeviceRc = _GetAdjustedWindowRect( new RECT { Bottom = 100, Right = 100 } );
          Point adjustedTopLeft = DpiHelper.DevicePixelsToLogical(
              new Point(
                  wp.rcNormalPosition.Left - adjustedDeviceRc.Left,
                  wp.rcNormalPosition.Top - adjustedDeviceRc.Top ) );

          _window.Top = adjustedTopLeft.Y;
          _window.Left = adjustedTopLeft.X;
        }
      }
    }

    private RECT _GetAdjustedWindowRect( RECT rcWindow )
    {
      // This should only be used to work around issues in the Framework that were fixed in 4.0
      Assert.IsTrue( Utility.IsPresentationFrameworkVersionLessThan4 );

      var style = ( WS )NativeMethods.GetWindowLongPtr( _hwnd, GWL.STYLE );
      var exstyle = ( WS_EX )NativeMethods.GetWindowLongPtr( _hwnd, GWL.EXSTYLE );

      return NativeMethods.AdjustWindowRectEx( rcWindow, style, false, exstyle );
    }

    // Windows tries hard to hide this state from applications.
    // Generally you can tell that the window is in a docked position because the restore bounds from GetWindowPlacement
    // don't match the current window location and it's not in a maximized or minimized state.
    // Because this isn't doced or supported, it's also not incredibly consistent.  Sometimes some things get updated in
    // different orders, so this isn't absolutely reliable.
    private bool _IsWindowDocked
    {
      get
      {
        // We're only detecting this state to work around .Net 3.5 issues.
        // This logic won't work correctly when those issues are fixed.
        Assert.IsTrue( Utility.IsPresentationFrameworkVersionLessThan4 );

        if( _window.WindowState != WindowState.Normal )
        {
          return false;
        }

        RECT adjustedOffset = _GetAdjustedWindowRect( new RECT { Bottom = 100, Right = 100 } );
        Point windowTopLeft = new Point( _window.Left, _window.Top );
        windowTopLeft -= ( Vector )DpiHelper.DevicePixelsToLogical( new Point( adjustedOffset.Left, adjustedOffset.Top ) );

        return _window.RestoreBounds.Location != windowTopLeft;
      }
    }

    #region WindowProc and Message Handlers

    private IntPtr _WndProc( IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled )
    {
      // Only expecting messages for our cached HWND.
      Assert.AreEqual( hwnd, _hwnd );

      var message = ( WM )msg;
      foreach( var handlePair in _messageTable )
      {
        if( handlePair.Key == message )
        {
          return handlePair.Value( message, wParam, lParam, out handled );
        }
      }
      return IntPtr.Zero;
    }

    private IntPtr _HandleSetTextOrIcon( WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled )
    {
      bool modified = _ModifyStyle( WS.VISIBLE, 0 );

      // Setting the caption text and icon cause Windows to redraw the caption.
      // Letting the default WndProc handle the message without the WS_VISIBLE
      // style applied bypasses the redraw.
      IntPtr lRet = NativeMethods.DefWindowProc( _hwnd, uMsg, wParam, lParam );

      // Put back the style we removed.
      if( modified )
      {
        _ModifyStyle( 0, WS.VISIBLE );
      }
      handled = true;
      return lRet;
    }

    private IntPtr _HandleNCActivate( WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled )
    {
      // Despite MSDN's documentation of lParam not being used,
      // calling DefWindowProc with lParam set to -1 causes Windows not to draw over the caption.

      // Directly call DefWindowProc with a custom parameter
      // which bypasses any other handling of the message.
      IntPtr lRet = NativeMethods.DefWindowProc( _hwnd, WM.NCACTIVATE, wParam, new IntPtr( -1 ) );
      handled = true;
      return lRet;
    }

    private IntPtr _HandleNCCalcSize( WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled )
    {
      // lParam is an [in, out] that can be either a RECT* (wParam == FALSE) or an NCCALCSIZE_PARAMS*.
      // Since the first field of NCCALCSIZE_PARAMS is a RECT and is the only field we care about
      // we can unconditionally treat it as a RECT.

      // Since we always want the client size to equal the window size, we can unconditionally handle it
      // without having to modify the parameters.

      handled = true;
      return new IntPtr( ( int )WVR.REDRAW );
    }

    private IntPtr _HandleNCHitTest( WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled )
    {
      IntPtr lRet = IntPtr.Zero;
      handled = false;

      // Give DWM a chance at this first.
      if( Utility.IsOSVistaOrNewer && _chromeInfo.GlassFrameThickness != default( Thickness ) && _isGlassEnabled )
      {
        // If we're on Vista, give the DWM a chance to handle the message first.
        handled = NativeMethods.DwmDefWindowProc( _hwnd, uMsg, wParam, lParam, out lRet );
      }

      // Handle letting the system know if we consider the mouse to be in our effective non-client area.
      // If DWM already handled this by way of DwmDefWindowProc, then respect their call.
      if( IntPtr.Zero == lRet )
      {
        var mousePosScreen = new Point( Utility.GET_X_LPARAM( lParam ), Utility.GET_Y_LPARAM( lParam ) );
        Rect windowPosition = _GetWindowRect();

        HT ht = _HitTestNca(
            DpiHelper.DeviceRectToLogical( windowPosition ),
            DpiHelper.DevicePixelsToLogical( mousePosScreen ) );

        // Don't blindly respect HTCAPTION.
        // We want UIElements in the caption area to be actionable so run through a hittest first.
        if( ht != HT.CLIENT )
        {
          Point mousePosWindow = mousePosScreen;
          mousePosWindow.Offset( -windowPosition.X, -windowPosition.Y );
          mousePosWindow = DpiHelper.DevicePixelsToLogical( mousePosWindow );
          IInputElement inputElement = _window.InputHitTest( mousePosWindow );
          if( inputElement != null && WindowChrome.GetIsHitTestVisibleInChrome( inputElement ) )
          {
            ht = HT.CLIENT;
          }
        }
        handled = true;
        lRet = new IntPtr( ( int )ht );
      }
      return lRet;
    }

    private IntPtr _HandleNCRButtonUp( WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled )
    {
      // Emulate the system behavior of clicking the right mouse button over the caption area
      // to bring up the system menu.
      if( HT.CAPTION == ( HT )wParam.ToInt32() )
      {
        if( _window.ContextMenu != null )
        {
          _window.ContextMenu.Placement = PlacementMode.MousePoint;
          _window.ContextMenu.IsOpen = true;
        }
        else if( WindowChrome.GetWindowChrome( _window ).ShowSystemMenu )
          SystemCommands.ShowSystemMenuPhysicalCoordinates( _window, new Point( Utility.GET_X_LPARAM( lParam ), Utility.GET_Y_LPARAM( lParam ) ) );
      }
      handled = false;
      return IntPtr.Zero;
    }

    private IntPtr _HandleSize( WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled )
    {
      const int SIZE_MAXIMIZED = 2;

      // Force when maximized.
      // We can tell what's happening right now, but the Window doesn't yet know it's
      // maximized.  Not forcing this update will eventually cause the
      // default caption to be drawn.
      WindowState? state = null;
      if( wParam.ToInt32() == SIZE_MAXIMIZED )
      {
        state = WindowState.Maximized;
      }
      _UpdateSystemMenu( state );

      // Still let the default WndProc handle this.
      handled = false;
      return IntPtr.Zero;
    }

    private IntPtr _HandleWindowPosChanged( WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled )
    {
      // http://blogs.msdn.com/oldnewthing/archive/2008/01/15/7113860.aspx
      // The WM_WINDOWPOSCHANGED message is sent at the end of the window
      // state change process. It sort of combines the other state change
      // notifications, WM_MOVE, WM_SIZE, and WM_SHOWWINDOW. But it doesn't
      // suffer from the same limitations as WM_SHOWWINDOW, so you can
      // reliably use it to react to the window being shown or hidden.

      _UpdateSystemMenu( null );

      if( !_isGlassEnabled )
      {
        Assert.IsNotDefault( lParam );
        var wp = ( WINDOWPOS )Marshal.PtrToStructure( lParam, typeof( WINDOWPOS ) );
        _SetRoundingRegion( wp );
      }

      // Still want to pass this to DefWndProc
      handled = false;
      return IntPtr.Zero;
    }

    private IntPtr _HandleDwmCompositionChanged( WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled )
    {
      _UpdateFrameState( false );

      handled = false;
      return IntPtr.Zero;
    }

    private IntPtr _HandleSettingChange( WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled )
    {
      // There are several settings that can cause fixups for the template to become invalid when changed.
      // These shouldn't be required on the v4 framework.
      Assert.IsTrue( Utility.IsPresentationFrameworkVersionLessThan4 );

      _FixupFrameworkIssues();

      handled = false;
      return IntPtr.Zero;
    }

    private IntPtr _HandleEnterSizeMove( WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled )
    {
      // This is only intercepted to deal with bugs in Window in .Net 3.5 and below.
      Assert.IsTrue( Utility.IsPresentationFrameworkVersionLessThan4 );

      _isUserResizing = true;

      // On Win7 if the user is dragging the window out of the maximized state then we don't want to use that location
      // as a restore point.
      Assert.Implies( _window.WindowState == WindowState.Maximized, Utility.IsOSWindows7OrNewer );
      if( _window.WindowState != WindowState.Maximized )
      {
        // Check for the docked window case.  The window can still be restored when it's in this position so 
        // try to account for that and not update the start position.
        if( !_IsWindowDocked )
        {
          _windowPosAtStartOfUserMove = new Point( _window.Left, _window.Top );
        }
        // Realistically we also don't want to update the start position when moving from one docked state to another (or to and from maximized),
        // but it's tricky to detect and this is already a workaround for a bug that's fixed in newer versions of the framework.
        // Not going to try to handle all cases.
      }

      handled = false;
      return IntPtr.Zero;
    }

    private IntPtr _HandleExitSizeMove( WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled )
    {
      // This is only intercepted to deal with bugs in Window in .Net 3.5 and below.
      Assert.IsTrue( Utility.IsPresentationFrameworkVersionLessThan4 );

      _isUserResizing = false;

      // On Win7 the user can change the Window's state by dragging the window to the top of the monitor.
      // If they did that, then we need to try to update the restore bounds or else WPF will put the window at the maximized location (e.g. (-8,-8)).
      if( _window.WindowState == WindowState.Maximized )
      {
        Assert.IsTrue( Utility.IsOSWindows7OrNewer );
        _window.Top = _windowPosAtStartOfUserMove.Y;
        _window.Left = _windowPosAtStartOfUserMove.X;
      }

      handled = false;
      return IntPtr.Zero;
    }

    private IntPtr _HandleMove( WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled )
    {
      // This is only intercepted to deal with bugs in Window in .Net 3.5 and below.
      Assert.IsTrue( Utility.IsPresentationFrameworkVersionLessThan4 );

      if( _isUserResizing )
      {
        _hasUserMovedWindow = true;
      }

      handled = false;
      return IntPtr.Zero;
    }

    #endregion

    /// <summary>Add and remove a native WindowStyle from the HWND.</summary>
    /// <param name="removeStyle">The styles to be removed.  These can be bitwise combined.</param>
    /// <param name="addStyle">The styles to be added.  These can be bitwise combined.</param>
    /// <returns>Whether the styles of the HWND were modified as a result of this call.</returns>
    private bool _ModifyStyle( WS removeStyle, WS addStyle )
    {
      Assert.IsNotDefault( _hwnd );
      var dwStyle = ( WS )NativeMethods.GetWindowLongPtr( _hwnd, GWL.STYLE ).ToInt32();
      var dwNewStyle = ( dwStyle & ~removeStyle ) | addStyle;
      if( dwStyle == dwNewStyle )
      {
        return false;
      }

      NativeMethods.SetWindowLongPtr( _hwnd, GWL.STYLE, new IntPtr( ( int )dwNewStyle ) );
      return true;
    }

    /// <summary>
    /// Get the WindowState as the native HWND knows it to be.  This isn't necessarily the same as what Window thinks.
    /// </summary>
    private WindowState _GetHwndState()
    {
      var wpl = NativeMethods.GetWindowPlacement( _hwnd );
      switch( wpl.showCmd )
      {
        case SW.SHOWMINIMIZED:
          return WindowState.Minimized;
        case SW.SHOWMAXIMIZED:
          return WindowState.Maximized;
      }
      return WindowState.Normal;
    }

    /// <summary>
    /// Get the bounding rectangle for the window in physical coordinates.
    /// </summary>
    /// <returns>The bounding rectangle for the window.</returns>
    private Rect _GetWindowRect()
    {
      // Get the window rectangle.
      RECT windowPosition = NativeMethods.GetWindowRect( _hwnd );
      return new Rect( windowPosition.Left, windowPosition.Top, windowPosition.Width, windowPosition.Height );
    }

    /// <summary>
    /// Update the items in the system menu based on the current, or assumed, WindowState.
    /// </summary>
    /// <param name="assumeState">
    /// The state to assume that the Window is in.  This can be null to query the Window's state.
    /// </param>
    /// <remarks>
    /// We want to update the menu while we have some control over whether the caption will be repainted.
    /// </remarks>
    private void _UpdateSystemMenu( WindowState? assumeState )
    {
      const MF mfEnabled = MF.ENABLED | MF.BYCOMMAND;
      const MF mfDisabled = MF.GRAYED | MF.DISABLED | MF.BYCOMMAND;

      WindowState state = assumeState ?? _GetHwndState();

      if( null != assumeState || _lastMenuState != state )
      {
        _lastMenuState = state;

        bool modified = _ModifyStyle( WS.VISIBLE, 0 );
        IntPtr hmenu = NativeMethods.GetSystemMenu( _hwnd, false );
        if( IntPtr.Zero != hmenu )
        {
          var dwStyle = ( WS )NativeMethods.GetWindowLongPtr( _hwnd, GWL.STYLE ).ToInt32();

          bool canMinimize = Utility.IsFlagSet( ( int )dwStyle, ( int )WS.MINIMIZEBOX );
          bool canMaximize = Utility.IsFlagSet( ( int )dwStyle, ( int )WS.MAXIMIZEBOX );
          bool canSize = Utility.IsFlagSet( ( int )dwStyle, ( int )WS.THICKFRAME );

          switch( state )
          {
            case WindowState.Maximized:
              NativeMethods.EnableMenuItem( hmenu, SC.RESTORE, mfEnabled );
              NativeMethods.EnableMenuItem( hmenu, SC.MOVE, mfDisabled );
              NativeMethods.EnableMenuItem( hmenu, SC.SIZE, mfDisabled );
              NativeMethods.EnableMenuItem( hmenu, SC.MINIMIZE, canMinimize ? mfEnabled : mfDisabled );
              NativeMethods.EnableMenuItem( hmenu, SC.MAXIMIZE, mfDisabled );
              break;
            case WindowState.Minimized:
              NativeMethods.EnableMenuItem( hmenu, SC.RESTORE, mfEnabled );
              NativeMethods.EnableMenuItem( hmenu, SC.MOVE, mfDisabled );
              NativeMethods.EnableMenuItem( hmenu, SC.SIZE, mfDisabled );
              NativeMethods.EnableMenuItem( hmenu, SC.MINIMIZE, mfDisabled );
              NativeMethods.EnableMenuItem( hmenu, SC.MAXIMIZE, canMaximize ? mfEnabled : mfDisabled );
              break;
            default:
              NativeMethods.EnableMenuItem( hmenu, SC.RESTORE, mfDisabled );
              NativeMethods.EnableMenuItem( hmenu, SC.MOVE, mfEnabled );
              NativeMethods.EnableMenuItem( hmenu, SC.SIZE, canSize ? mfEnabled : mfDisabled );
              NativeMethods.EnableMenuItem( hmenu, SC.MINIMIZE, canMinimize ? mfEnabled : mfDisabled );
              NativeMethods.EnableMenuItem( hmenu, SC.MAXIMIZE, canMaximize ? mfEnabled : mfDisabled );
              break;
          }
        }

        if( modified )
        {
          _ModifyStyle( 0, WS.VISIBLE );
        }
      }
    }

    private void _UpdateFrameState( bool force )
    {
      if( IntPtr.Zero == _hwnd )
      {
        return;
      }

      // Don't rely on SystemParameters2 for this, just make the check ourselves.
      bool frameState = NativeMethods.DwmIsCompositionEnabled();

      if( force || frameState != _isGlassEnabled )
      {
        _isGlassEnabled = frameState && _chromeInfo.GlassFrameThickness != default( Thickness );

        if( !_isGlassEnabled )
        {
          _SetRoundingRegion( null );
        }
        else
        {
          _ClearRoundingRegion();
          _ExtendGlassFrame();

          _FixupWindows7Issues();
        }

        NativeMethods.SetWindowPos( _hwnd, IntPtr.Zero, 0, 0, 0, 0, _SwpFlags );
      }
    }

    private void _ClearRoundingRegion()
    {
      NativeMethods.SetWindowRgn( _hwnd, IntPtr.Zero, NativeMethods.IsWindowVisible( _hwnd ) );
    }

    private void _SetRoundingRegion( WINDOWPOS? wp )
    {
      const int MONITOR_DEFAULTTONEAREST = 0x00000002;

      // We're early - WPF hasn't necessarily updated the state of the window.
      // Need to query it ourselves.
      WINDOWPLACEMENT wpl = NativeMethods.GetWindowPlacement( _hwnd );

      if( wpl.showCmd == SW.SHOWMAXIMIZED )
      {
        int left;
        int top;

        if( wp.HasValue )
        {
          left = wp.Value.x;
          top = wp.Value.y;
        }
        else
        {
          Rect r = _GetWindowRect();
          left = ( int )r.Left;
          top = ( int )r.Top;
        }

        IntPtr hMon = NativeMethods.MonitorFromWindow( _hwnd, MONITOR_DEFAULTTONEAREST );

        MONITORINFO mi = NativeMethods.GetMonitorInfo( hMon );
        RECT rcMax = mi.rcWork;
        // The location of maximized window takes into account the border that Windows was
        // going to remove, so we also need to consider it.
        rcMax.Offset( -left, -top );

        IntPtr hrgn = IntPtr.Zero;
        try
        {
          hrgn = NativeMethods.CreateRectRgnIndirect( rcMax );
          NativeMethods.SetWindowRgn( _hwnd, hrgn, NativeMethods.IsWindowVisible( _hwnd ) );
          hrgn = IntPtr.Zero;
        }
        finally
        {
          Utility.SafeDeleteObject( ref hrgn );
        }
      }
      else
      {
        Size windowSize;

        // Use the size if it's specified.
        if( null != wp && !Utility.IsFlagSet( wp.Value.flags, ( int )SWP.NOSIZE ) )
        {
          windowSize = new Size( ( double )wp.Value.cx, ( double )wp.Value.cy );
        }
        else if( null != wp && ( _lastRoundingState == _window.WindowState ) )
        {
          return;
        }
        else
        {
          windowSize = _GetWindowRect().Size;
        }

        _lastRoundingState = _window.WindowState;

        IntPtr hrgn = IntPtr.Zero;
        try
        {
          double shortestDimension = Math.Min( windowSize.Width, windowSize.Height );

          double topLeftRadius = DpiHelper.LogicalPixelsToDevice( new Point( _chromeInfo.CornerRadius.TopLeft, 0 ) ).X;
          topLeftRadius = Math.Min( topLeftRadius, shortestDimension / 2 );

          if( _IsUniform( _chromeInfo.CornerRadius ) )
          {
            // RoundedRect HRGNs require an additional pixel of padding.
            hrgn = _CreateRoundRectRgn( new Rect( windowSize ), topLeftRadius );
          }
          else
          {
            // We need to combine HRGNs for each of the corners.
            // Create one for each quadrant, but let it overlap into the two adjacent ones
            // by the radius amount to ensure that there aren't corners etched into the middle
            // of the window.
            hrgn = _CreateRoundRectRgn( new Rect( 0, 0, windowSize.Width / 2 + topLeftRadius, windowSize.Height / 2 + topLeftRadius ), topLeftRadius );

            double topRightRadius = DpiHelper.LogicalPixelsToDevice( new Point( _chromeInfo.CornerRadius.TopRight, 0 ) ).X;
            topRightRadius = Math.Min( topRightRadius, shortestDimension / 2 );
            Rect topRightRegionRect = new Rect( 0, 0, windowSize.Width / 2 + topRightRadius, windowSize.Height / 2 + topRightRadius );
            topRightRegionRect.Offset( windowSize.Width / 2 - topRightRadius, 0 );
            Assert.AreEqual( topRightRegionRect.Right, windowSize.Width );

            _CreateAndCombineRoundRectRgn( hrgn, topRightRegionRect, topRightRadius );

            double bottomLeftRadius = DpiHelper.LogicalPixelsToDevice( new Point( _chromeInfo.CornerRadius.BottomLeft, 0 ) ).X;
            bottomLeftRadius = Math.Min( bottomLeftRadius, shortestDimension / 2 );
            Rect bottomLeftRegionRect = new Rect( 0, 0, windowSize.Width / 2 + bottomLeftRadius, windowSize.Height / 2 + bottomLeftRadius );
            bottomLeftRegionRect.Offset( 0, windowSize.Height / 2 - bottomLeftRadius );
            Assert.AreEqual( bottomLeftRegionRect.Bottom, windowSize.Height );

            _CreateAndCombineRoundRectRgn( hrgn, bottomLeftRegionRect, bottomLeftRadius );

            double bottomRightRadius = DpiHelper.LogicalPixelsToDevice( new Point( _chromeInfo.CornerRadius.BottomRight, 0 ) ).X;
            bottomRightRadius = Math.Min( bottomRightRadius, shortestDimension / 2 );
            Rect bottomRightRegionRect = new Rect( 0, 0, windowSize.Width / 2 + bottomRightRadius, windowSize.Height / 2 + bottomRightRadius );
            bottomRightRegionRect.Offset( windowSize.Width / 2 - bottomRightRadius, windowSize.Height / 2 - bottomRightRadius );
            Assert.AreEqual( bottomRightRegionRect.Right, windowSize.Width );
            Assert.AreEqual( bottomRightRegionRect.Bottom, windowSize.Height );

            _CreateAndCombineRoundRectRgn( hrgn, bottomRightRegionRect, bottomRightRadius );
          }

          NativeMethods.SetWindowRgn( _hwnd, hrgn, NativeMethods.IsWindowVisible( _hwnd ) );
          hrgn = IntPtr.Zero;
        }
        finally
        {
          // Free the memory associated with the HRGN if it wasn't assigned to the HWND.
          Utility.SafeDeleteObject( ref hrgn );
        }
      }
    }

    private static IntPtr _CreateRoundRectRgn( Rect region, double radius )
    {
      // Round outwards.

      if( DoubleUtilities.AreClose( 0, radius ) )
      {
        return NativeMethods.CreateRectRgn(
            ( int )Math.Floor( region.Left ),
            ( int )Math.Floor( region.Top ),
            ( int )Math.Ceiling( region.Right ),
            ( int )Math.Ceiling( region.Bottom ) );
      }

      // RoundedRect HRGNs require an additional pixel of padding on the bottom right to look correct.
      return NativeMethods.CreateRoundRectRgn(
          ( int )Math.Floor( region.Left ),
          ( int )Math.Floor( region.Top ),
          ( int )Math.Ceiling( region.Right ) + 1,
          ( int )Math.Ceiling( region.Bottom ) + 1,
          ( int )Math.Ceiling( radius ),
          ( int )Math.Ceiling( radius ) );
    }

    [SuppressMessage( "Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "HRGNs" )]
    private static void _CreateAndCombineRoundRectRgn( IntPtr hrgnSource, Rect region, double radius )
    {
      IntPtr hrgn = IntPtr.Zero;
      try
      {
        hrgn = _CreateRoundRectRgn( region, radius );
        CombineRgnResult result = NativeMethods.CombineRgn( hrgnSource, hrgnSource, hrgn, RGN.OR );
        if( result == CombineRgnResult.ERROR )
        {
          throw new InvalidOperationException( "Unable to combine two HRGNs." );
        }
      }
      catch
      {
        Utility.SafeDeleteObject( ref hrgn );
        throw;
      }
    }

    private static bool _IsUniform( CornerRadius cornerRadius )
    {
      if( !DoubleUtilities.AreClose( cornerRadius.BottomLeft, cornerRadius.BottomRight ) )
      {
        return false;
      }

      if( !DoubleUtilities.AreClose( cornerRadius.TopLeft, cornerRadius.TopRight ) )
      {
        return false;
      }

      if( !DoubleUtilities.AreClose( cornerRadius.BottomLeft, cornerRadius.TopRight ) )
      {
        return false;
      }

      return true;
    }

    private void _ExtendGlassFrame()
    {
      Assert.IsNotNull( _window );

      // Expect that this might be called on OSes other than Vista.
      if( !Utility.IsOSVistaOrNewer )
      {
        // Not an error.  Just not on Vista so we're not going to get glass.
        return;
      }

      if( IntPtr.Zero == _hwnd )
      {
        // Can't do anything with this call until the Window has been shown.
        return;
      }

      // Ensure standard HWND background painting when DWM isn't enabled.
      if( !NativeMethods.DwmIsCompositionEnabled() )
      {
        _hwndSource.CompositionTarget.BackgroundColor = SystemColors.WindowColor;
      }
      else
      {
        // This makes the glass visible at a Win32 level so long as nothing else is covering it.
        // The Window's Background needs to be changed independent of this.

        // Apply the transparent background to the HWND
        _hwndSource.CompositionTarget.BackgroundColor = Colors.Transparent;

        // Thickness is going to be DIPs, need to convert to system coordinates.
        Point deviceTopLeft = DpiHelper.LogicalPixelsToDevice( new Point( _chromeInfo.GlassFrameThickness.Left, _chromeInfo.GlassFrameThickness.Top ) );
        Point deviceBottomRight = DpiHelper.LogicalPixelsToDevice( new Point( _chromeInfo.GlassFrameThickness.Right, _chromeInfo.GlassFrameThickness.Bottom ) );

        var dwmMargin = new MARGINS
        {
          // err on the side of pushing in glass an extra pixel.
          cxLeftWidth = ( int )Math.Ceiling( deviceTopLeft.X ),
          cxRightWidth = ( int )Math.Ceiling( deviceBottomRight.X ),
          cyTopHeight = ( int )Math.Ceiling( deviceTopLeft.Y ),
          cyBottomHeight = ( int )Math.Ceiling( deviceBottomRight.Y ),
        };

        NativeMethods.DwmExtendFrameIntoClientArea( _hwnd, ref dwmMargin );
      }
    }

    /// <summary>
    /// Matrix of the HT values to return when responding to NC window messages.
    /// </summary>
    [SuppressMessage( "Microsoft.Performance", "CA1814:PreferJaggedArraysOverMultidimensional", MessageId = "Member" )]
    private static readonly HT[,] _HitTestBorders = new[ , ]
    {
            { HT.TOPLEFT,    HT.TOP,     HT.TOPRIGHT    },
            { HT.LEFT,       HT.CLIENT,  HT.RIGHT       },
            { HT.BOTTOMLEFT, HT.BOTTOM,  HT.BOTTOMRIGHT },
        };

    private HT _HitTestNca( Rect windowPosition, Point mousePosition )
    {
      // Determine if hit test is for resizing, default middle (1,1).
      int uRow = 1;
      int uCol = 1;
      bool onResizeBorder = false;

      // Determine if the point is at the top or bottom of the window.
      if( mousePosition.Y >= windowPosition.Top && mousePosition.Y < windowPosition.Top + _chromeInfo.ResizeBorderThickness.Top + _chromeInfo.CaptionHeight )
      {
        onResizeBorder = ( mousePosition.Y < ( windowPosition.Top + _chromeInfo.ResizeBorderThickness.Top ) );
        uRow = 0; // top (caption or resize border)
      }
      else if( mousePosition.Y < windowPosition.Bottom && mousePosition.Y >= windowPosition.Bottom - ( int )_chromeInfo.ResizeBorderThickness.Bottom )
      {
        uRow = 2; // bottom
      }

      // Determine if the point is at the left or right of the window.
      if( mousePosition.X >= windowPosition.Left && mousePosition.X < windowPosition.Left + ( int )_chromeInfo.ResizeBorderThickness.Left )
      {
        uCol = 0; // left side
      }
      else if( mousePosition.X < windowPosition.Right && mousePosition.X >= windowPosition.Right - _chromeInfo.ResizeBorderThickness.Right )
      {
        uCol = 2; // right side
      }

      // If the cursor is in one of the top edges by the caption bar, but below the top resize border,
      // then resize left-right rather than diagonally.
      if( uRow == 0 && uCol != 1 && !onResizeBorder )
      {
        uRow = 1;
      }

      HT ht = _HitTestBorders[ uRow, uCol ];

      if( ht == HT.TOP && !onResizeBorder )
      {
        ht = HT.CAPTION;
      }

      return ht;
    }

    #region Remove Custom Chrome Methods

    private void _RestoreStandardChromeState( bool isClosing )
    {
      VerifyAccess();

      _UnhookCustomChrome();

      if( !isClosing )
      {
        _RestoreFrameworkIssueFixups();
        _RestoreGlassFrame();
        _RestoreHrgn();

        _window.InvalidateMeasure();
      }
    }

    private void _UnhookCustomChrome()
    {
      Assert.IsNotDefault( _hwnd );
      Assert.IsNotNull( _window );

      if( _isHooked )
      {
        _hwndSource.RemoveHook( _WndProc );
        _isHooked = false;
      }
    }

    private void _RestoreFrameworkIssueFixups()
    {
      // This margin is only necessary if the client rect is going to be calculated incorrectly by WPF.
      // This bug was fixed in V4 of the framework.
      if( Utility.IsPresentationFrameworkVersionLessThan4 )
      {
        Assert.IsTrue( _isFixedUp );

        var rootElement = ( FrameworkElement )VisualTreeHelper.GetChild( _window, 0 );
        if( rootElement != null )
        {
          // Undo anything that was done before.
          rootElement.Margin = new Thickness();
        }

        _window.StateChanged -= _FixupRestoreBounds;
        _isFixedUp = false;
      }
    }

    private void _RestoreGlassFrame()
    {
      Assert.IsNull( _chromeInfo );
      Assert.IsNotNull( _window );

      // Expect that this might be called on OSes other than Vista
      // and if the window hasn't yet been shown, then we don't need to undo anything.
      if( !Utility.IsOSVistaOrNewer || _hwnd == IntPtr.Zero )
      {
        return;
      }

      _hwndSource.CompositionTarget.BackgroundColor = SystemColors.WindowColor;

      if( NativeMethods.DwmIsCompositionEnabled() )
      {
        // If glass is enabled, push it back to the normal bounds.
        var dwmMargin = new MARGINS();
        NativeMethods.DwmExtendFrameIntoClientArea( _hwnd, ref dwmMargin );
      }
    }

    private void _RestoreHrgn()
    {
      _ClearRoundingRegion();
      NativeMethods.SetWindowPos( _hwnd, IntPtr.Zero, 0, 0, 0, 0, _SwpFlags );
    }

    #endregion
  }
}
